<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <title>Dialog Tutorial</title>
  <link rel="jsbin" href="https://jsbin.com/qawasag/">
  <style id="example-css">

    /*
      For styling overlays you may want to have a look at
      http://tympanus.net/codrops/2013/11/07/css-overlay-techniques/
    */

    /*
      make the dialog container spans the entire screen,
      this is necessary to capture clicks
    */
    #dialog {
      position: fixed;
      top: 0;
      right: 0;
      bottom: 0;
      left: 0;
      background: rgba(0,0,0,0);
      z-index: 999;
    }
    /*
      obscure everything but the dialog
      unless the backdrop needs a special animation,
      this element could be ignored and the backdrop
      simply applied to the #dialog element via
      background: rgba(0, 0, 0, 0.25);
    */
    #dialog:before {
      content: "";
      position: fixed;
      top: 0;
      right: 0;
      bottom: 0;
      left: 0;
      background: rgba(0, 0, 0, 0.25);
      z-index: 999;

      -webkit-transition: opacity 0.2s ease-in-out;
              transition: opacity 0.2s ease-in-out;
    }
    /* show the dialog in the center of the screen */
    #dialog .dialog-content {
      box-sizing: border-box;
      /* maintain on screen even during scroll (problematic on mobile) */
      position: fixed;
      /* when centering, try using FlexBox instead of this junk */
      top: 50%;
      left: 50%;
      width: 300px;
      height: 200px;
      -webkit-transform: translate3d(-50%,-50%,0);
              transform: translate3d(-50%,-50%,0);

      /* dialogs are usually box things that are on top of everything */
      padding: 20px;
      border: 1px solid #CCC;
      background: white;
      z-index: 1000;

      -webkit-transition: transform 0.2s ease-in-out;
              transition: transform 0.2s ease-in-out;
    }

    /* style the dialog's contents */
    #dialog h1 {
      margin: 0;
    }
    #dialog footer {
      margin-top: 20px;
    }

    /* hide the dialog in a way that allows animations */
    #dialog[hidden],
    #dialog[hidden]:before,
    #dialog[hidden] .dialog-content {
      display: block;
      visibility: hidden;
      -webkit-transform: translate3d(0px, -1px, 0px) scale(0);
              transform: translate3d(0px, -1px, 0px) scale(0);

      -webkit-transition:
        visibility 0s linear 0.2s,
        transform 0s linear 0.2s;
      transition:
        visibility 0s linear 0.2s,
        transform 0s linear 0.2s;
    }
    #dialog[hidden]:before {
      opacity: 0;
      /*
        overwrite the backdrops's showing transition so that dimensions
        are maintained until the box finished its opacity transformation
      */
      -webkit-transition:
        opacity 0.2s ease-in-out,
        visibility 0s linear 0.2s,
        transform 0s linear 0.2s;
      transition:
        opacity 0.2s ease-in-out,
        visibility 0s linear 0.2s,
        transform 0s linear 0.2s;
    }
    #dialog[hidden] .dialog-content {
      -webkit-transform: translate3d(0px, -1px, 0px) scale(0);
              transform: translate3d(0px, -1px, 0px) scale(0);
      /*
        overwrite the dialog's showing transition so that visibility
        is maintained until the box finished its transformation
      */
      -webkit-transition:
        transform 0.2s ease-in-out,
        visibility 0s linear 0.2s;
      transition:
        transform 0.2s ease-in-out,
        visibility 0s linear 0.2s;
    }
  </style>
</head>
<body>

<article id="example-introduction">
  <h1>Dialog Tutorial</h1>

  <p>View the source of this page to see how a visually appealing, yet fully accessible dialog can be created. The rules for accessible dialogs are laid out in <a href="https://www.w3.org/WAI/PF/aria-practices/#dialog_modal">WAI-ARIA 1.0 Authoring Practices</a></p>
  <p>There are two input fields surrounding the "open dialog" button to mimic interactive content outside of the dialog. They have no other purpose than serve as proof of focus being trapped within the dialog once it's shown.</p>
</article>

<div id="example-html">
  <main>
    <div>
      <label for="before-button">Demo Text Input before Dialog Button</label> <input id="before-button">
    </div>
    <section>
      <button type="button" id="open-dialog">Show Dialog</button>
    </section>

    <label for="after-button">Demo Text Input after Dialog Button</label> <input id="after-button">
  </main>

  <!--
    If not using the <dialog> element of HTML5.1, it's best to use the <div> element for widgets
    with semantic meaning that can only be defined via aria.

    It is not uncommon to wrap the dialog's content in a <form>,
    provide the confirmation button <button type="submit">OK</button>,
    and wire up the submit event to close the dialog after processing
    the user's input (while preventing the form submission itself).

    This has the added benefit of <kbd>Enter</kbd> working out of the
    box, without JavaScript having to listen for the key to be pressed.
    Also touchscreen devices (such as modern mobile phones) have
    built-in heuristics that change the appearance of OnScreenKeyboards
    depending on <form> and <input> type.

    When dealing with elements that contain focus, it is a good idea to make them focusable themselves
    by adding tabindex="-1". This way a mouse click in the background of the dialog won't pass focus
    to the document, but retain it in the dialog.

    The aria attributes role, aria-labeledby and aria-describedby are necessary for the Accessibility Tree
    to properly expose our dialog widget. Their specific meaning is explained here:

    * https://www.w3.org/TR/wai-aria/roles#dialog
    * https://www.w3.org/TR/wai-aria/states_and_properties#aria-labelledby
    * https://www.w3.org/TR/wai-aria/states_and_properties#aria-describedby
  -->
  <div id="dialog" role="dialog" aria-labelledby="dialog-title" aria-describedby="dialog-description" tabindex="-1" hidden>
    <form class="dialog-content">
      <header>
        <!--
          The dialog's header explains what the user is being interrupted for.
          While a label is considered mandatory, an extended description is optional.
        -->
        <h1 id="dialog-title">Name Entry</h1>
        <p id="dialog-description">Please enter your full name.</p>
      </header>
      <section>
        <!--
          A dialog can do anything from displaying simple text, to hosting
          complex user interaction. But since the latter is generally
          considered bad practice, try keeping your dialogs as simple
          as possible.
        -->
        <label for="within-dialog">Name</label> <input id="within-dialog">
      </section>
      <footer>
        <!--
          the footer of a dialog usually contains its control buttons
          to close, confirm or do something else entirely.
        -->
        <button type="button" id="close-dialog">Close</button>
        <button type="submit" id="save-dialog">Save</button>
      </footer>
    </form>
  </div>
</div>

<script src="https://cdn.jsdelivr.net/ally.js/1.4.1/ally.min.js"></script>

<script id="example-js">
  // Grab the elements we need to interact with
  var openButton = document.getElementById('open-dialog');
  var dialog = document.getElementById('dialog');
  var closeButton = document.getElementById('close-dialog');

  // Data filled by openDialog() and later used by closeDialog()
  var keyHandle;
  var tabHandle;
  var disabledHandle;
  var hiddenHandle;
  var focusedElementBeforeDialogOpened;

  function openDialog() {
    // Remember the focused element before we opened the dialog
    // so we can return focus to it once we close the dialog.
    focusedElementBeforeDialogOpened = document.activeElement;

    // We're using a transition to reveal the dialog,
    // so wait until the element is visible, before
    // finding the first keyboard focusable element
    // and passing focus to it, otherwise the browser
    // might scroll the document to reveal the element
    // receiving focus
    ally.when.visibleArea({
      context: dialog,
      callback: function(context) {
        // the dialog is visible on screen, so find the first
        // keyboard focusable element (giving any element with
        // autofocus attribute precendence). If the dialog does
        // not contain any keyboard focusabe elements, focus will
        // be given to the dialog itself.
        var element = ally.query.firstTabbable({
          context: context, // context === dialog
          defaultToContext: true,
        });
        element.focus();
      },
    });

    // Make sure that no element outside of the dialog
    // can be interacted with while the dialog is visible.
    // This means we don't have to handle Tab and Shift+Tab,
    // but can defer that to the browser's internal handling.
    disabledHandle = ally.maintain.disabled({
      filter: dialog,
    });

    // Make sure that no element outside of the dialog
    // is exposed via the Accessibility Tree, to prevent
    // screen readers from navigating to content it shouldn't
    // be seeing while the dialog is open. See example:
    // https://marcysutton.com/slides/mobile-a11y-seattlejs/#/36
    hiddenHandle = ally.maintain.hidden({
      filter: dialog,
    });

    // Make sure that Tab key controlled focus is trapped within
    // the tabsequence of the dialog and does not reach the
    // browser's UI, e.g. the location bar.
    tabHandle = ally.maintain.tabFocus({
      context: dialog,
    });

    // React to enter and escape keys as mandated by ARIA Practices
    keyHandle = ally.when.key({
      escape: closeDialogByKey,
      // Note: in non-interactive content you would also bind the enter
      // key to close the dialog. In our example the form's submit event
      // will cover that instead. The enter handler would be executed
      // for *every* press of the enter key, even if the focused element
      // is a button (which would invoke the default action). Try using
      // a <form> for interactive content to get around that problem.
      // enter: closeDialogByKey,
    });

    // Show the dialog
    dialog.hidden = false;
  }

  function closeDialogByKey() {
    // we need to let the keyboard event handlers finish,
    // before actually closing the dialog. Otherwise the
    // keydown of <kbd>enter</kbd> will close the dialog,
    // focus is passed back to the open-dialog-button and
    // the keyup of <kbd>enter</kbd> will open the dialog
    // again.
    setTimeout(closeDialog);
    // alternatively we could've called event.preventDefault()
    // and then run closeDialog() synchronously
  }

  function closeDialog() {
    // undo listening to keyboard
    keyHandle.disengage();
    // undo trapping Tab key focus
    tabHandle.disengage();
    // undo hiding elements outside of the dialog
    hiddenHandle.disengage();
    // undo disabling elements outside of the dialog
    disabledHandle.disengage();
    // return focus to where it was before we opened the dialog
    focusedElementBeforeDialogOpened.focus();
    // hide or remove the dialog
    dialog.hidden = true;
  }

  function saveDialog(event) {
    // do not submit the form
    event.preventDefault();

    // do something with the entered data
    var name = dialog.querySelector('input').value;
    console.log('entered name', name);

    closeDialog();
  }

  // wire up showing/hiding the dialog
  openButton.addEventListener('click', openDialog, false);
  closeButton.addEventListener('click', closeDialog, false);
  dialog.addEventListener('submit', saveDialog, true);
</script>

</body>
</html>
